<?php
declare (strict_types = 1);

namespace app\command;

use app\common\JsonException;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;

class ServerAdminAuthLogin extends Command
{
    protected $address;
    protected $server;
    protected $pid_file;
    protected $log_file;

    protected function configure()
    {
        $protocol = empty(config('app.server_adminauthlogin.enable_ssl')) ? 'ws' : 'wss';
        $host = config('app.server_adminauthlogin.host');
        $port = config('app.server_adminauthlogin.port');
        $this->address = "<{$protocol}://{$host}:{$port}>";

        // 指令配置
        $this->setName('server:adminauthlogin')
            ->addArgument('action', Argument::OPTIONAL, "start|stop|reload|restart|status", 'start')
            ->setDescription('后台扫码登录 ' . $this->address);

        $this->log_file = runtime_path() . 'log' . DIRECTORY_SEPARATOR . 'adminauthlogin.log';
        // pid文件不要放到runtime目录，否则容易被清除掉，造成进程无法正常关闭
        $this->pid_file = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'marks' . DIRECTORY_SEPARATOR . 'adminauthlogin.pid';
    }

    protected function execute(Input $input, Output $output)
    {
        if (!extension_loaded('swoole')) {
            $output->error('Can\'t detect Swoole extension installed.');
            return;
        }
        $action = $input->getArgument('action');
        if (in_array($action, ['start', 'stop', 'reload', 'restart', 'status'])) {
            $this->$action();
        } else {
            $output->error("Invalid argument action:{$action}, Expected start|stop|reload|restart|status.");
            return;
        }
    }

    protected function checkFile($file)
    {
        $dir = dirname($file);
        if (!is_dir($dir)) {
            @mkdir($dir, 0700, true);
        }
        @touch($file);
        if (!is_writable($file)) {
            return false;
        }
        return true;
    }

    protected function pushMsg($fd, $action, $data = null)
    {
        $result = [
            'type' => 1,
            'action' => $action,
            'reqid' => '',
            'code' => 0,
            'msg' => '',
            'data' => $data,
        ];
        return $this->server->push($fd, json_encode($result, JSON_UNESCAPED_UNICODE));
    }

    protected function responseMsg($fd, $action, $reqid, $code = 1, $msg = null, $data = null)
    {
        if (is_null($msg)) {
            $msg = lang("ERROR_CLIENT_RESP_{$code}");
        }
        $result = [
            'type' => 0,
            'action' => $action,
            'reqid' => $reqid,
            'code' => (int) $code,
            'msg' => $msg,
            'data' => $data,
        ];
        $this->server->push($fd, json_encode($result, JSON_UNESCAPED_UNICODE));
    }

    protected function action_refreshQrcode($fd, $data)
    {
        $fd_info = $this->server->getClientInfo($fd);
        $flag = rand_str(4);
        $info = [
            'flag' => $flag,
            'fd' => $fd,
            'ip' => empty($fd_info['remote_ip']) ? '0.0.0.0' : $fd_info['remote_ip'],
        ];
        $udid = sign_data($info, '', false);
        $cacheId = "cache_qrcode_info_{$udid}";
        cache($cacheId, $info, 300);
        $url = site_url('wechat/admin_auth.html?udid=' . $udid);
        success([
            'flag' => $flag,
            'qrcode' => bin2hex($url),
        ]);
    }

    protected function start()
    {
        if ($this->isRunning()) {
            $this->output->error('Swoole server is already running.');
            return;
        }
        $this->output->writeln('Starting swoole server...');

        if (!$this->checkFile($this->pid_file)) {
            $this->output->error("Can\'t write file: {$this->pid_file}.");
            return;
        }
        if (!$this->checkFile($this->log_file)) {
            $this->output->error("Can\'t write file: {$this->log_file}.");
            return;
        }

        $sockType = SWOOLE_SOCK_TCP;
        $option = array(
            'worker_num' => 2,
            'pid_file' => $this->pid_file,
            'log_file' => $this->log_file,
            'daemonize' => true,
            'heartbeat_check_interval' => 3600,
            'heartbeat_idle_time' => 86400,
        );
        if (!empty(config('app.server_adminauthlogin.enable_ssl'))) {
            $sockType = $sockType | SWOOLE_SSL;
            $option['ssl_cert_file'] = config('app.server_adminauthlogin.cert_file');
            $option['ssl_key_file'] = config('app.server_adminauthlogin.key_file');
        }
        $this->server = new \swoole_websocket_server(config('app.server_adminauthlogin.host'), (int) config('app.server_adminauthlogin.port'), SWOOLE_PROCESS, $sockType);
        $this->server->set($option);

        $this->server->on('Open', function ($server, $request) {
        });

        $this->server->on('Message', function ($server, $frame) {
            $data = null;
            try
            {
                $data = json_decode($frame->data, true);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    failure(10000);
                }
                //参数校验
                if (empty($data['action'])) {
                    failure(10002);
                }
                //处理动作
                $func = "action_" . $data['action'];
                if (method_exists($this, $func)) {
                    $this->$func($frame->fd, $data);
                    success();
                }
                failure(10004);

            } catch (JsonException $e) {
                $reqid = '';
                if (isset($data['reqid'])) {
                    $reqid = $data['reqid'];
                }
                $action = 'RESPONSE';
                if (isset($data['action'])) {
                    $action = $data['action'];
                }
                $json = $e->getJson();
                $this->responseMsg($frame->fd, $action, $reqid, $json['code'], $json['msg'], $json['data']);
            }
        });

        $this->server->on('Request', function ($request, $response) {
            try {
                $data = json_decode($request->rawContent(), true);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    failure(10000);
                }
                if (empty($data)) {
                    failure(10);
                }
                if (!verify_sign($data, config('app.server_adminauthlogin.secret'))) {
                    failure(11);
                }
                if (!verify_timestamp($data, 300)) {
                    failure(12);
                }
                sign_clear($data);
                $clientInfo = $data['clientInfo'];
                $msgAction = $data['msgAction'];
                $msgData = $data['msgData'];
                if (!empty($clientInfo['fd'])) {
                    $fd = (int) $clientInfo['fd'];
                    if (!$this->server->isEstablished($fd)) {
                        failure(111);
                    }
                    $result = $this->pushMsg($fd, $msgAction, $msgData);
                    if (!$result) {
                        failure(112);
                    }
                    success();
                }
                failure(121);
            } catch (JsonException $e) {
                $json = $e->getJson();
                if (is_null($json['msg'])) {
                    $code = $json['code'];
                    $json['msg'] = lang("ERROR_{$code}");
                }
                $response->write(json_encode($json, JSON_UNESCAPED_UNICODE));
            }
        });

        $this->server->on('Close', function ($server, $fd) {
        });

        $this->output->writeln("Swoole server started: {$this->address}");

        $this->server->start();
    }

    protected function stop()
    {
        if (!$this->isRunning()) {
            $this->output->error('No swoole server running.');
            return;
        }
        $this->output->writeln('Stopping swoole server...');
        $pid = (int) file_get_contents($this->pid_file);
        \swoole_process::kill($pid, SIGTERM);
        $end = time() + 15;
        while (time() < $end && \swoole_process::kill($pid, 0)) {
            usleep(100000);
        }
        if ($this->isRunning()) {
            $this->output->error('Unable to stop the swoole server.');
            return;
        }
        $this->output->writeln('Stopping success.');
    }

    protected function reload()
    {
        if (!$this->isRunning()) {
            $this->output->error('No swoole server running.');
            return;
        }
        $this->output->writeln('Reloading swoole server...');
        $pid = (int) file_get_contents($this->pid_file);
        if (!\swoole_process::kill($pid, SIGUSR1)) {
            $this->output->error('Reloading failure.');
            return;
        }
        $this->output->writeln('Reloading success.');
    }

    protected function restart()
    {
        if ($this->isRunning()) {
            $this->stop();
        }
        $this->start();
    }

    protected function status()
    {
        $this->output->writeln($this->isRunning() ? 'Server is running.' : 'Server stopped.');
    }

    protected function isRunning()
    {
        if (!is_readable($this->pid_file)) {
            return false;
        }
        $pid = (int) file_get_contents($this->pid_file);
        return $pid > 0 && \swoole_process::kill($pid, 0);
    }
}
